@hoardodile/create-plugin 0.1.3 → 0.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -43,12 +43,15 @@ jobs:
43
43
  - name: Package the release artifacts
44
44
  run: pnpm exec hoardodile plugin package --skip-build
45
45
 
46
+ - name: Gate the introduction (intro/ must be flat, images by bare filename)
47
+ run: pnpm intro:check
48
+
46
49
  - name: Publish the GitHub release
47
50
  env:
48
51
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
49
52
  run: |
50
53
  files=$(ls release/*)
51
- if ls intro.*.md >/dev/null 2>&1; then
52
- files="$files intro.*.md"
54
+ if [ -d intro ]; then
55
+ files="$files intro/*"
53
56
  fi
54
57
  gh release create "$GITHUB_REF_NAME" $files --generate-notes
@@ -100,13 +100,34 @@ private packages.
100
100
  ## Publishing an introduction
101
101
 
102
102
  The marketplace detail view shows a per-release **Intro** tab. Ship one
103
- markdown file per supported language at the repository root, named
104
- `intro.<locale>.md` (e.g. `intro.en.md`, `intro.zh.md`) — `release.yml`
105
- uploads them alongside the zip, so **each release carries its own
106
- introduction** and every version shows independent notes. Use the app's
107
- supported language codes as file names (`en`, `zh`, `ja`, `de`, `es`) —
108
- a region-coded name like `intro.zh-CN.md` only matches a UI language
109
- resolved to that exact code, so `intro.zh.md` is what Chinese users see.
103
+ markdown file per supported language inside the **`intro/` folder**, named
104
+ `intro.<locale>.md` (e.g. `intro/intro.en.md`, `intro/intro.zh.md`) —
105
+ `release.yml` uploads the whole folder alongside the zip, so **each release
106
+ carries its own introduction** and every version shows independent notes.
107
+ Use the app's supported language codes as file names (`en`, `zh`, `ja`,
108
+ `de`, `es`) — a region-coded name like `intro.zh-CN.md` only matches a UI
109
+ language resolved to that exact code, so `intro.zh.md` is what Chinese
110
+ users see.
111
+
112
+ ### Adding images
113
+
114
+ An introduction may reference images. Place the image in `intro/` and
115
+ reference it by its **bare filename**:
116
+
117
+ ```md
118
+ ![Plugin screenshot](screenshot.png)
119
+ ```
120
+
121
+ Every file in `intro/` is published as a release asset on each release, and
122
+ the app resolves a relative image reference against that release's download
123
+ URL. Because a GitHub release is a flat list of assets, the `intro/` folder
124
+ must stay **flat** and references must be bare filenames — a nested path
125
+ like `![alt](img/shot.png)` resolves to a URL the release does not serve and
126
+ the image breaks. Absolute `http(s)://` and `data:` image URIs are allowed.
127
+
128
+ `pnpm intro:check` (run by `release.yml` before publishing) gates this: it
129
+ fails the release if `intro/` is absent-and-required, is not flat, ships no
130
+ `intro.<locale>.md`, or references an image by a nested/missing path.
110
131
 
111
132
  The app resolves the intro for the user's UI language (exact locale → base
112
133
  language → `en` → the only shipped language); the release body always shows
@@ -11,6 +11,7 @@
11
11
  "detect:smoke": "hoardodile plugin run detect testdata --plugin-dir dist",
12
12
  "lint": "tsc --noEmit",
13
13
  "test": "vitest run",
14
+ "intro:check": "node scripts/check-intro.mjs",
14
15
  "release": "node scripts/release.mjs"
15
16
  },
16
17
  "release-it": {
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Release gate for the plugin marketplace introduction images.
4
+ *
5
+ * The marketplace reads each release's `intro.<locale>.md` asset and the
6
+ * app resolves any image referenced inside it against the release's
7
+ * download URL. Because a GitHub release is a flat list of assets, every
8
+ * referenced image must be:
9
+ *
10
+ * 1. Shipped inside the `intro/` folder (the only folder the release
11
+ * workflow uploads), and
12
+ * 2. Referenced by its bare filename (`![alt](shot.png)`), never a
13
+ * nested path (`img/shot.png`) — a nested path resolves to a URL the
14
+ * release does not actually serve, so the image is silently broken.
15
+ *
16
+ * This gate fails a build/release when the `intro/` folder is absent, is
17
+ * not flat, ships no `intro.<locale>.md`, or references an image by a
18
+ * nested/missing path. External `http(s)://` and `data:` image URIs are
19
+ * allowed (they are not release assets).
20
+ *
21
+ * Usage:
22
+ * node scripts/check-intro.mjs # checks ./intro
23
+ * node scripts/check-intro.mjs <dir> # checks <dir>/intro
24
+ *
25
+ * Dependency-free on purpose — it ships inside every scaffolded plugin.
26
+ */
27
+
28
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"
29
+ import { join, resolve } from "node:path"
30
+
31
+ const ROOT = resolve(process.argv[2] ?? process.cwd())
32
+ const INTRO_DIR = join(ROOT, "intro")
33
+
34
+ const RULE_SUMMARY =
35
+ "intro/ must be flat; each intro.<locale>.md image is referenced by a " +
36
+ "bare filename that exists in intro/ (absolute http(s)/data URIs are ok)"
37
+
38
+ // `![alt](src)`, `![alt](src "title")`, `<img src="x">` (`src` images only).
39
+ const MARKDOWN_IMG_RE = /!\[[^\]]*\]\(([^)\s]+)(?:\s+["'][^"']*["'])?\)/g
40
+ const HTML_IMG_RE = /<img\b[^>]*\bsrc\s*=\s*(["'])([^"']+)\1/gi
41
+ const EXT_URL_RE = /^(?:https?:\/\/|data:)/i
42
+
43
+ function isRelative(src) {
44
+ return !EXT_URL_RE.test(src)
45
+ }
46
+
47
+ /**
48
+ * A relative reference is valid only as a flat bare filename that exists
49
+ * inside `intro/` — not a nested path and not a missing/unshipsed file.
50
+ */
51
+ function resolveRelativeRef(src, file, issues) {
52
+ const trimmed = src.trim().replace(/^\.\//, "")
53
+ if (trimmed.length === 0) {
54
+ issues.push(`${file}: empty image reference`)
55
+ return
56
+ }
57
+ if (
58
+ trimmed.includes("/") ||
59
+ trimmed.includes("\\") ||
60
+ trimmed.includes("..")
61
+ ) {
62
+ issues.push(
63
+ `${file}: image "${src}" is not flat — use a bare filename like "shot.png" (release assets are flat)`,
64
+ )
65
+ return
66
+ }
67
+ const target = join(INTRO_DIR, trimmed)
68
+ if (!existsSync(target) || !statSync(target).isFile()) {
69
+ issues.push(
70
+ `${file}: image "${src}" is not in intro/ — every referenced image must be committed there (it is published with the release)`,
71
+ )
72
+ }
73
+ }
74
+
75
+ function collectImageRefs(text) {
76
+ const refs = []
77
+ for (const match of text.matchAll(MARKDOWN_IMG_RE)) refs.push(match[1])
78
+ for (const match of text.matchAll(HTML_IMG_RE)) refs.push(match[2])
79
+ return refs
80
+ }
81
+
82
+ function main() {
83
+ if (!existsSync(INTRO_DIR)) {
84
+ console.log(
85
+ "[check-intro] no intro/ folder — nothing to gate (a release without an introduction is valid).",
86
+ )
87
+ return
88
+ }
89
+
90
+ const entries = readdirSync(INTRO_DIR, { withFileTypes: true })
91
+ const issues = []
92
+
93
+ // 1. Flat-only: any subdirectory makes the folder publish incorrectly.
94
+ for (const entry of entries) {
95
+ if (entry.isDirectory()) {
96
+ issues.push(
97
+ `intro/${entry.name}/ is a subdirectory — the intro folder must be flat (release assets are a flat list)`,
98
+ )
99
+ }
100
+ }
101
+
102
+ const mdFiles = entries
103
+ .filter(
104
+ (entry) =>
105
+ entry.isFile() && /^intro\.[A-Za-z0-9-]+\.md$/.test(entry.name),
106
+ )
107
+ .map((entry) => entry.name)
108
+
109
+ if (mdFiles.length === 0) {
110
+ issues.push(
111
+ "intro/ has no intro.<locale>.md — the release would ship images but the marketplace could not display an introduction",
112
+ )
113
+ }
114
+
115
+ // 2. Flat, present image references inside each intro markdown.
116
+ for (const name of mdFiles) {
117
+ const text = readFileSync(join(INTRO_DIR, name), "utf-8")
118
+ for (const src of collectImageRefs(text)) {
119
+ if (isRelative(src)) resolveRelativeRef(src, `intro/${name}`, issues)
120
+ }
121
+ }
122
+
123
+ if (issues.length > 0) {
124
+ console.error("[check-intro] gate failed:")
125
+ for (const issue of issues) console.error(` - ${issue}`)
126
+ console.error(`\n${RULE_SUMMARY}`)
127
+ process.exit(1)
128
+ }
129
+
130
+ console.log(
131
+ `[check-intro] intro/ ok — ${mdFiles.length} introduction file(s), flat references only.`,
132
+ )
133
+ }
134
+
135
+ main()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hoardodile/create-plugin",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "license": "MIT",
5
5
  "description": "Scaffold a hoardodile content plugin.",
6
6
  "keywords": [
@@ -37,7 +37,7 @@
37
37
  "dependencies": {
38
38
  "@clack/prompts": "^1.7.0",
39
39
  "zod": "^4.4.3",
40
- "@hoardodile/sdk-types": "0.1.3"
40
+ "@hoardodile/sdk-types": "0.1.5"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "^26.2.0",